<!-- @format -->

<template>
  <div class="comp-upload" :class="{ 'is-disabled': disabled }">
    <!-- 上传按钮 -->
    <div
      class="comp-upload__button"
      @click="handleClick"
      v-if="!disabled || fileList.length < limit"
    >
      <!-- 自定义上传按钮插槽 -->
      <slot name="upload-button">
        <div class="comp-upload__button-default">
          <span class="comp-upload__icon">+</span>
          <span class="comp-upload__text">{{ buttonText }}</span>
        </div>
      </slot>
    </div>

    <!-- 文件列表 -->
    <div class="comp-upload__list" v-if="showFileList && fileList.length > 0">
      <div
        v-for="(file, index) in fileList"
        :key="file.uid"
        class="comp-upload__item"
        :class="{
          'is-uploading': file.status === 'uploading',
          'is-success': file.status === 'success',
          'is-error': file.status === 'error',
        }"
      >
        <!-- 文件信息 -->
        <div class="comp-upload__info" @click="handlePreview(file)">
          <!-- 文件图标/预览图 -->
          <div class="comp-upload__thumbnail" v-if="listType !== 'text'">
            <img
              v-if="file.url && isImageFile(file)"
              :src="file.url"
              alt="thumbnail"
            />
            <div v-else class="comp-upload__file-icon">
              {{ getFileIconText(file) }}
            </div>
          </div>

          <!-- 文件名称和大小 -->
          <div class="comp-upload__details">
            <div class="comp-upload__name">{{ file.name }}</div>
            <div class="comp-upload__size">{{ formatFileSize(file.size) }}</div>
          </div>
        </div>

        <!-- 上传进度 -->
        <div
          class="comp-upload__progress"
          v-if="showProgress && file.status === 'uploading'"
        >
          <div class="comp-upload__progress-bar">
            <div
              class="comp-upload__progress-inner"
              :style="{ width: `${file.progress}%` }"
            ></div>
          </div>
          <div class="comp-upload__progress-text">{{ file.progress }}%</div>
        </div>

        <!-- 操作按钮 -->
        <div class="comp-upload__actions">
          <!-- 重试按钮 -->
          <span
            v-if="file.status === 'error'"
            class="comp-upload__action comp-upload__retry"
            @click="handleRetry(file)"
          >
            重试
          </span>

          <!-- 删除按钮 -->
          <span
            class="comp-upload__action comp-upload__delete"
            @click="handleRemove(file, index)"
          >
            删除
          </span>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
  import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue';
  import { generateUUID } from './utils';
  import type { UploadFile, UploadStatus, UploadMethod } from './types';

  // 定义组件属性
  const props = withDefaults(
    defineProps<{
      // 上传的文件列表，双向绑定
      modelValue: UploadFile[];
      // 上传的地址
      action: string;
      // 设置上传的请求头部
      headers?: Record<string, string>;
      // 上传时附带的额外参数
      data?: Record<string, any>;
      // 上传的文件字段名
      name?: string;
      // 是否支持多选文件
      multiple?: boolean;
      // 是否在选取文件后立即进行上传
      autoUpload?: boolean;
      // 接受上传的文件类型
      accept?: string;
      // 文件大小限制，单位为字节
      maxSize?: number;
      // 最大允许上传文件数量
      limit?: number;
      // 是否禁用
      disabled?: boolean;
      // 上传按钮文字
      buttonText?: string;
      // 上传方法
      uploadMethod?: UploadMethod;
      // 是否显示文件列表
      showFileList?: boolean;
      // 是否显示上传进度
      showProgress?: boolean;
      // 列表的类型
      listType?: 'text' | 'picture' | 'picture-card';
      // 上传请求超时时间
      timeout?: number;
    }>(),
    {
      headers: () => ({}),
      data: () => ({}),
      name: 'file',
      multiple: false,
      autoUpload: true,
      accept: '',
      maxSize: 0, // 0表示不限制大小
      limit: Infinity,
      disabled: false,
      buttonText: '点击上传',
      uploadMethod: 'form',
      showFileList: true,
      showProgress: true,
      listType: 'text',
      timeout: 30000,
    },
  );

  // 定义事件
  const emit = defineEmits<{
    // 更新文件列表（v-model）
    (e: 'update:modelValue', files: UploadFile[]): void;
    // 文件状态改变时的钩子
    (e: 'change', file: UploadFile, fileList: UploadFile[]): void;
    // 文件上传成功时的钩子
    (
      e: 'success',
      response: any,
      file: UploadFile,
      fileList: UploadFile[],
    ): void;
    // 文件上传失败时的钩子
    (e: 'error', error: any, file: UploadFile, fileList: UploadFile[]): void;
    // 文件上传进度变化时的钩子
    (e: 'progress', event: any, file: UploadFile, fileList: UploadFile[]): void;
    // 文件超出个数限制时的钩子
    (e: 'exceed', files: any[], fileList: UploadFile[]): void;
    // 删除文件时的钩子
    (e: 'remove', file: UploadFile, fileList: UploadFile[]): void;
    // 点击文件列表中已上传的文件时的钩子
    (e: 'preview', file: UploadFile): void;
  }>();

  // 内部状态
  const fileList = ref<UploadFile[]>([]);
  const uploadTasks = ref<Record<string, any>>({});

  // 监听文件列表变化，同步到v-model
  watch(
    () => props.modelValue,
    (newVal) => {
      if (newVal !== fileList.value) {
        fileList.value = [...newVal];
      }
    },
    { deep: true, immediate: true },
  );

  // 监听内部文件列表变化，同步到v-model
  watch(
    fileList,
    (newVal) => {
      emit('update:modelValue', [...newVal]);
    },
    { deep: true },
  );

  // 处理点击上传按钮
  const handleClick = () => {
    if (props.disabled) return;

    // 根据平台选择不同的选择文件方法
    // #ifdef APP-PLUS
    // App端使用uni.chooseFile
    uni.chooseFile({
      count: props.multiple
        ? Math.min(9, props.limit - fileList.value.length)
        : 1,
      type: 'all',
      extension: props.accept
        .split(',')
        .map((ext) => ext.trim().replace('.', '')),
      success: (res) => {
        handleFiles(res.tempFiles);
      },
      fail: (err) => {
        console.error('选择文件失败', err);
      },
    });
    // #endif

    // #ifdef H5
    // H5端使用input元素
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = props.accept;
    input.multiple = props.multiple;
    input.style.display = 'none';

    input.onchange = (e: any) => {
      const files = e.target.files;
      if (!files) return;

      // 转换File对象为统一格式
      const tempFiles = Array.from(files).map((file: any) => ({
        name: file.name,
        path: URL.createObjectURL(file),
        size: file.size,
        type: file.type,
        file: file, // 保存原始File对象
      }));

      handleFiles(tempFiles);
      document.body.removeChild(input);
    };

    document.body.appendChild(input);
    input.click();
    // #endif

    // #ifdef MP-WEIXIN
    // 微信小程序根据文件类型选择不同的API
    if (props.accept.includes('image')) {
      // 选择图片
      uni.chooseImage({
        count: props.multiple
          ? Math.min(9, props.limit - fileList.value.length)
          : 1,
        sizeType: ['original', 'compressed'],
        sourceType: ['album', 'camera'],
        success: (res) => {
          const tempFiles = res.tempFiles.map((file: any) => ({
            name: file.path.split('/').pop(),
            path: file.path,
            size: file.size,
            type: 'image/' + (file.path.split('.').pop() || 'jpeg'),
          }));
          handleFiles(tempFiles);
        },
        fail: (err) => {
          console.error('选择图片失败', err);
        },
      });
    } else if (props.accept.includes('video')) {
      // 选择视频
      uni.chooseVideo({
        sourceType: ['album', 'camera'],
        compressed: true,
        maxDuration: 60,
        camera: 'back',
        success: (res) => {
          const tempFiles = [
            {
              name: res.tempFilePath.split('/').pop(),
              path: res.tempFilePath,
              size: res.size,
              type: 'video/' + (res.tempFilePath.split('.').pop() || 'mp4'),
            },
          ];
          handleFiles(tempFiles);
        },
        fail: (err) => {
          console.error('选择视频失败', err);
        },
      });
    } else {
      // 其他文件类型，微信小程序限制较多
      uni.showToast({
        title: '微信小程序仅支持选择图片或视频',
        icon: 'none',
      });
    }
    // #endif
  };

  // 处理选择的文件
  const handleFiles = (files: any) => {
    // 检查文件数量限制
    if (files.length + fileList.value.length > props.limit) {
      emit('exceed', files, fileList.value);
      return;
    }

    // 处理每个文件
    const newFiles: UploadFile[] = [];

    files.forEach((file: any) => {
      // 检查文件大小
      if (props.maxSize > 0 && file.size > props.maxSize) {
        uni.showToast({
          title: `文件 ${file.name} 超出大小限制`,
          icon: 'none',
        });
        return;
      }

      // 创建文件对象
      const uploadFile: UploadFile = {
        uid: generateUUID(),
        name: file.name,
        size: file.size,
        type: file.type || getFileType(file.name),
        status: 'ready',
        progress: 0,
        path: file.path,
        raw: file,
      };

      // 如果是图片，生成预览URL
      if (isImageFile(uploadFile)) {
        uploadFile.url = file.path;
      }

      newFiles.push(uploadFile);
      fileList.value.push(uploadFile);

      // 触发change事件
      emit('change', uploadFile, fileList.value);
    });

    // 自动上传
    if (props.autoUpload && newFiles.length > 0) {
      uploadFiles(newFiles);
    }
  };

  // 上传文件
  const uploadFiles = (files: UploadFile[]) => {
    files.forEach((file) => {
      if (file.status === 'ready') {
        uploadFile(file);
      }
    });
  };

  // 上传单个文件
  const uploadFile = (file: UploadFile) => {
    // 更新文件状态
    updateFileStatus(file, 'uploading');

    // 根据上传方法选择不同的上传方式
    if (props.uploadMethod === 'base64' && isImageFile(file)) {
      uploadFileAsBase64(file);
    } else {
      uploadFileAsForm(file);
    }
  };

  // 表单方式上传文件
  const uploadFileAsForm = (file: UploadFile) => {
    // #ifdef H5
    // H5平台使用FormData和XMLHttpRequest
    if (file.raw && file.raw.file instanceof File) {
      const formData = new FormData();

      // 添加额外数据
      if (props.data) {
        Object.keys(props.data).forEach((key) => {
          formData.append(key, props.data[key]);
        });
      }

      // 添加文件
      formData.append(props.name, file.raw.file, file.name);

      // 创建XMLHttpRequest
      const xhr = new XMLHttpRequest();
      xhr.open('POST', props.action, true);

      // 设置超时
      if (props.timeout) {
        xhr.timeout = props.timeout;
      }

      // 设置请求头
      if (props.headers) {
        Object.keys(props.headers).forEach((key) => {
          xhr.setRequestHeader(key, props.headers[key]);
        });
      }

      // 上传进度
      xhr.upload.onprogress = (e) => {
        if (e.lengthComputable) {
          const progress = Math.round((e.loaded * 100) / e.total);
          file.progress = progress;
          emit('progress', { progress }, file, fileList.value);
        }
      };

      // 请求完成
      xhr.onload = () => {
        if (xhr.status >= 200 && xhr.status < 300) {
          let response;
          try {
            response = JSON.parse(xhr.responseText);
          } catch (e) {
            response = xhr.responseText;
          }

          // 上传成功
          handleUploadSuccess(response, file);
        } else {
          // 上传失败
          handleUploadError(new Error(`请求错误 (${xhr.status})`), file);
        }

        // 清理上传任务
        delete uploadTasks.value[file.uid];
      };

      // 请求错误
      xhr.onerror = () => {
        handleUploadError(new Error('网络错误'), file);
        delete uploadTasks.value[file.uid];
      };

      // 请求超时
      xhr.ontimeout = () => {
        handleUploadError(new Error('请求超时'), file);
        delete uploadTasks.value[file.uid];
      };

      // 发送请求
      xhr.send(formData);

      // 保存上传任务引用，用于取消上传
      uploadTasks.value[file.uid] = {
        abort: () => xhr.abort(),
      };
    } else {
      handleUploadError(new Error('文件格式不支持'), file);
    }
    // #endif

    // #ifndef H5
    // 非H5平台使用uni.uploadFile
    const uploadTask = uni.uploadFile({
      url: props.action,
      filePath: file.path,
      name: props.name,
      header: props.headers,
      formData: { ...props.data },
      timeout: props.timeout,
      success: (res) => {
        let response;
        try {
          response = JSON.parse(res.data);
        } catch (e) {
          response = res.data;
        }

        // 上传成功
        handleUploadSuccess(response, file);
      },
      fail: (err) => {
        // 上传失败
        handleUploadError(err, file);
      },
      complete: () => {
        // 清理上传任务
        delete uploadTasks.value[file.uid];
      },
    });

    // 保存上传任务引用，用于取消上传
    uploadTasks.value[file.uid] = uploadTask;

    // 监听上传进度
    uploadTask.onProgressUpdate((res) => {
      file.progress = res.progress;
      emit('progress', res, file, fileList.value);
    });
    // #endif
  };

  // Base64方式上传图片
  const uploadFileAsBase64 = (file: UploadFile) => {
    // #ifdef H5
    // H5平台使用FileReader API
    if (file.raw && file.raw.file instanceof File) {
      const reader = new FileReader();
      reader.onload = (e) => {
        const base64Data = e.target?.result as string;

        // 发起网络请求上传Base64数据
        const requestTask = uni.request({
          url: props.action,
          method: 'POST',
          header: {
            'content-type': 'application/json',
            ...props.headers,
          },
          data: {
            ...props.data,
            [props.name]: base64Data,
            filename: file.name,
          },
          timeout: props.timeout,
          success: (res) => {
            // 上传成功
            handleUploadSuccess(res.data, file);
          },
          fail: (err) => {
            // 上传失败
            handleUploadError(err, file);
          },
          complete: () => {
            // 清理上传任务
            delete uploadTasks.value[file.uid];
          },
        });

        // 保存上传任务引用，用于取消上传
        uploadTasks.value[file.uid] = requestTask;
      };

      reader.onerror = (err) => {
        // 读取文件失败
        handleUploadError(new Error('读取文件失败'), file);
      };

      reader.readAsDataURL(file.raw.file);
    } else {
      handleUploadError(new Error('文件格式不支持'), file);
    }
    // #endif

    // #ifndef H5
    // 非H5平台使用uni.getFileSystemManager
    uni.getFileSystemManager().readFile({
      filePath: file.path as string,
      encoding: 'base64',
      success: (res) => {
        const base64Data = `data:${file.type};base64,${res.data}`;

        // 发起网络请求上传Base64数据
        const requestTask = uni.request({
          url: props.action,
          method: 'POST',
          header: {
            'content-type': 'application/json',
            ...props.headers,
          },
          data: {
            ...props.data,
            [props.name]: base64Data,
            filename: file.name,
          },
          timeout: props.timeout,
          success: (res) => {
            // 上传成功
            handleUploadSuccess(res.data, file);
          },
          fail: (err) => {
            // 上传失败
            handleUploadError(err, file);
          },
          complete: () => {
            // 清理上传任务
            delete uploadTasks.value[file.uid];
          },
        });

        // 保存上传任务引用，用于取消上传
        uploadTasks.value[file.uid] = requestTask;
      },
      fail: (err) => {
        // 读取文件失败
        handleUploadError(err, file);
      },
    });
    // #endif
  };

  // 处理上传成功
  const handleUploadSuccess = (response: any, file: UploadFile) => {
    file.status = 'success';
    file.response = response;
    file.progress = 100;

    emit('success', response, file, fileList.value);
    emit('change', file, fileList.value);
  };

  // 处理上传失败
  const handleUploadError = (error: any, file: UploadFile) => {
    file.status = 'error';
    file.error = typeof error === 'string' ? error : error.errMsg || '上传失败';

    emit('error', error, file, fileList.value);
    emit('change', file, fileList.value);
  };

  // 更新文件状态
  const updateFileStatus = (file: UploadFile, status: UploadStatus) => {
    file.status = status;
    emit('change', file, fileList.value);
  };

  // 处理文件预览
  const handlePreview = (file: UploadFile) => {
    emit('preview', file);

    // 如果是图片且有URL，则预览图片
    if (file.url && isImageFile(file)) {
      uni.previewImage({
        urls: [file.url],
        current: file.url,
      });
    }
  };

  // 处理重试上传
  const handleRetry = (file: UploadFile) => {
    file.status = 'ready';
    file.progress = 0;
    file.error = '';

    uploadFile(file);
  };

  // 处理移除文件
  const handleRemove = (file: UploadFile, index: number) => {
    // 取消上传中的任务
    if (file.status === 'uploading' && uploadTasks.value[file.uid]) {
      // 所有平台都支持abort方法
      if (typeof uploadTasks.value[file.uid].abort === 'function') {
        uploadTasks.value[file.uid].abort();
      }
      delete uploadTasks.value[file.uid];
    }

    // 从列表中移除
    fileList.value.splice(index, 1);

    emit('remove', file, fileList.value);
    emit('change', file, fileList.value);
  };

  // 手动上传文件
  const submitUpload = () => {
    const pendingFiles = fileList.value.filter(
      (file) => file.status === 'ready',
    );
    if (pendingFiles.length > 0) {
      uploadFiles(pendingFiles);
    }
  };

  // 清空文件列表
  const clearFiles = () => {
    // 取消所有上传中的任务
    fileList.value.forEach((file) => {
      if (file.status === 'uploading' && uploadTasks.value[file.uid]) {
        // 所有平台都支持abort方法
        if (typeof uploadTasks.value[file.uid].abort === 'function') {
          uploadTasks.value[file.uid].abort();
        }
        delete uploadTasks.value[file.uid];
      }
    });

    // 清空列表
    fileList.value = [];
  };

  // 取消上传请求
  const abort = (file?: UploadFile) => {
    if (file) {
      // 取消指定文件的上传
      if (uploadTasks.value[file.uid]) {
        // 所有平台都支持abort方法
        if (typeof uploadTasks.value[file.uid].abort === 'function') {
          uploadTasks.value[file.uid].abort();
        }
        delete uploadTasks.value[file.uid];
        updateFileStatus(file, 'error');
        file.error = '上传已取消';
      }
    } else {
      // 取消所有上传中的任务
      Object.keys(uploadTasks.value).forEach((uid) => {
        uploadTasks.value[uid].abort();
        const targetFile = fileList.value.find((file) => file.uid === uid);
        if (targetFile) {
          updateFileStatus(targetFile, 'error');
          targetFile.error = '上传已取消';
        }
      });
      uploadTasks.value = {};
    }
  };

  // 判断是否为图片文件
  const isImageFile = (file: UploadFile) => {
    return file.type.indexOf('image') !== -1;
  };

  // 获取文件类型
  const getFileType = (fileName: string) => {
    const extension = fileName.split('.').pop()?.toLowerCase() || '';
    const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
    const videoExts = ['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv'];
    const audioExts = ['mp3', 'wav', 'ogg', 'aac', 'flac'];

    if (imageExts.includes(extension)) return 'image/' + extension;
    if (videoExts.includes(extension)) return 'video/' + extension;
    if (audioExts.includes(extension)) return 'audio/' + extension;

    return 'application/octet-stream';
  };

  // 获取文件图标文本
  const getFileIconText = (file: UploadFile) => {
    if (file.type.indexOf('image') !== -1) return '图片';
    if (file.type.indexOf('video') !== -1) return '视频';
    if (file.type.indexOf('audio') !== -1) return '音频';
    if (file.type.indexOf('pdf') !== -1) return 'PDF';
    if (
      file.type.indexOf('word') !== -1 ||
      file.name.endsWith('.doc') ||
      file.name.endsWith('.docx')
    )
      return 'DOC';
    if (
      file.type.indexOf('excel') !== -1 ||
      file.name.endsWith('.xls') ||
      file.name.endsWith('.xlsx')
    )
      return 'XLS';
    if (
      file.type.indexOf('powerpoint') !== -1 ||
      file.name.endsWith('.ppt') ||
      file.name.endsWith('.pptx')
    )
      return 'PPT';
    if (
      file.type.indexOf('zip') !== -1 ||
      file.type.indexOf('rar') !== -1 ||
      file.name.endsWith('.zip') ||
      file.name.endsWith('.rar')
    )
      return 'ZIP';
    return '文件';
  };

  // 格式化文件大小
  const formatFileSize = (size: number) => {
    if (size < 1024) {
      return size + 'B';
    } else if (size < 1024 * 1024) {
      return (size / 1024).toFixed(2) + 'KB';
    } else if (size < 1024 * 1024 * 1024) {
      return (size / (1024 * 1024)).toFixed(2) + 'MB';
    } else {
      return (size / (1024 * 1024 * 1024)).toFixed(2) + 'GB';
    }
  };

  // 组件卸载前清理所有上传任务
  onBeforeUnmount(() => {
    Object.keys(uploadTasks.value).forEach((uid) => {
      if (uploadTasks.value[uid]) {
        uploadTasks.value[uid].abort();
      }
    });
    uploadTasks.value = {};
  });

  // 暴露组件方法
  defineExpose({
    submitUpload,
    clearFiles,
    abort,
  });
</script>

<style lang="scss" scoped>
  .comp-upload {
    display: flex;
    flex-direction: column;

    &.is-disabled {
      opacity: 0.7;
      cursor: not-allowed;

      .comp-upload__button {
        pointer-events: none;
      }
    }

    &__button {
      display: inline-flex;
      cursor: pointer;

      &-default {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        width: 100px;
        height: 100px;
        border: 1px dashed #d9d9d9;
        border-radius: 4px;
        background-color: #fafafa;
        transition: border-color 0.3s;

        &:hover {
          border-color: #1989fa;
        }
      }
    }

    &__icon {
      font-size: 28px;
      color: #8c939d;
      margin-bottom: 8px;
    }

    &__text {
      font-size: 14px;
      color: #606266;
    }

    &__list {
      margin-top: 10px;
    }

    &__item {
      display: flex;
      align-items: center;
      margin-bottom: 8px;
      padding: 8px 10px;
      border-radius: 4px;
      background-color: #f5f7fa;
      transition: background-color 0.3s;

      &.is-uploading {
        background-color: #e6f7ff;
      }

      &.is-success {
        background-color: #f0f9eb;
      }

      &.is-error {
        background-color: #fef0f0;
      }
    }

    &__info {
      display: flex;
      align-items: center;
      flex: 1;
      min-width: 0;
      cursor: pointer;
    }

    &__thumbnail {
      width: 40px;
      height: 40px;
      margin-right: 10px;
      border-radius: 4px;
      overflow: hidden;
      background-color: #fff;
      display: flex;
      align-items: center;
      justify-content: center;

      img {
        width: 100%;
        height: 100%;
        object-fit: cover;
      }
    }

    &__file-icon {
      width: 100%;
      height: 100%;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 12px;
      color: #606266;
      background-color: #ebeef5;
    }

    &__details {
      flex: 1;
      min-width: 0;
    }

    &__name {
      font-size: 14px;
      color: #303133;
      margin-bottom: 4px;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }

    &__size {
      font-size: 12px;
      color: #909399;
    }

    &__progress {
      display: flex;
      align-items: center;
      margin: 0 10px;
      width: 120px;

      &-bar {
        flex: 1;
        height: 4px;
        background-color: #ebeef5;
        border-radius: 2px;
        overflow: hidden;
      }

      &-inner {
        height: 100%;
        background-color: #1989fa;
        border-radius: 2px;
        transition: width 0.3s;
      }

      &-text {
        margin-left: 5px;
        font-size: 12px;
        color: #606266;
        width: 36px;
        text-align: right;
      }
    }

    &__actions {
      display: flex;
      align-items: center;
    }

    &__action {
      font-size: 12px;
      color: #409eff;
      cursor: pointer;
      margin-left: 10px;

      &:hover {
        color: #66b1ff;
      }

      &.comp-upload__delete:hover {
        color: #f56c6c;
      }
    }
  }
</style>
